package org.codefinger.dao.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.codefinger.dao.impl.sql.ColumnItem;
import org.codefinger.dao.impl.sql.ColumnSeparatorItem;
import org.codefinger.dao.impl.sql.DefaultSource;
import org.codefinger.dao.impl.sql.Expression;
import org.codefinger.dao.impl.sql.GroupItem;
import org.codefinger.dao.impl.sql.Having;
import org.codefinger.dao.impl.sql.IValue;
import org.codefinger.dao.impl.sql.IValueItem;
import org.codefinger.dao.impl.sql.JoinItem;
import org.codefinger.dao.impl.sql.OrderItem;
import org.codefinger.dao.impl.sql.Source;
import org.codefinger.dao.impl.sql.SubQuerySource;
import org.codefinger.dao.impl.sql.UValue;
import org.codefinger.dao.impl.sql.UValueItem;
import org.codefinger.dao.impl.sql.UnionItem;
import org.codefinger.dao.impl.sql.Where;
import org.codefinger.dao.impl.sql.ast.AggregationExpression;
import org.codefinger.dao.impl.sql.ast.AliasExpression;
import org.codefinger.dao.impl.sql.ast.BetweenExpression;
import org.codefinger.dao.impl.sql.ast.BracketExpression;
import org.codefinger.dao.impl.sql.ast.CompareExpression;
import org.codefinger.dao.impl.sql.ast.FunctionExpression;
import org.codefinger.dao.impl.sql.ast.IfCaseExpression;
import org.codefinger.dao.impl.sql.ast.IndexAllLikeExpression;
import org.codefinger.dao.impl.sql.ast.IndexLeftLikeExpression;
import org.codefinger.dao.impl.sql.ast.IndexListExpression;
import org.codefinger.dao.impl.sql.ast.IndexParamExpression;
import org.codefinger.dao.impl.sql.ast.IsNotNullExpression;
import org.codefinger.dao.impl.sql.ast.IsNullExpression;
import org.codefinger.dao.impl.sql.ast.LeftExpression;
import org.codefinger.dao.impl.sql.ast.ListExpression;
import org.codefinger.dao.impl.sql.ast.MinusExpression;
import org.codefinger.dao.impl.sql.ast.NamedAllLikeExpression;
import org.codefinger.dao.impl.sql.ast.NamedLeftLikeExpression;
import org.codefinger.dao.impl.sql.ast.NamedListExpression;
import org.codefinger.dao.impl.sql.ast.NamedParamExpression;
import org.codefinger.dao.impl.sql.ast.SubQueryExpression;
import org.codefinger.dao.impl.sql.ast.SwitchCaseExpression;
import org.codefinger.dao.impl.sql.ast.TextExpression;
import org.codefinger.dao.impl.sql.ast.TwoExpression;
import org.codefinger.dao.util.ArrayIterator;
import org.codefinger.dao.util.Lang;

public abstract class QueryBuilder {

	protected DataAccesser			dataAccesser;

	protected Expression[]			expStack	= new Expression[10];

	protected int					expStackPo;

	protected boolean				isDistinct;

	protected Set<String>			columnAliases;

	protected List<ColumnItem>		columns;

	protected List<String>			aliases;

	protected List<ColumnCache>		columnCaches;

	protected boolean				columnFlag;

	protected Map<String, String>	tableAliasMap;

	protected Source				from;

	protected String				subQueryAlias;

	protected ArrayIterator<String>	subColumnsAlias;

	protected List<JoinItem>		joins;

	protected Where					where;

	protected List<GroupItem>		groups;

	protected boolean				groupFlag;

	protected Having				having;

	protected List<UnionItem>		unions;

	protected List<OrderItem>		orders;

	protected List<UValueItem>		uValues;

	protected List<IValueItem>		iValues;

	protected String				table;

	public QueryBuilder(DataAccesser dataAccesser) {
		super();
		this.dataAccesser = dataAccesser;
		this.columnAliases = new HashSet<String>();
		this.columns = new ArrayList<ColumnItem>(10);
		this.aliases = new ArrayList<String>(10);
		this.columnCaches = new ArrayList<ColumnCache>(10);
		this.tableAliasMap = new HashMap<String, String>();
	}

	public QueryBuilder(List<UValueItem> uValues) {
		this.uValues = uValues;
	}

	public QueryBuilder(DataAccesser dataAccesser, String table) {
		this.subQueryAlias = dataAccesser.getTableInfo(table).getPrimaryKey();
		this.table = table;
		this.iValues = new ArrayList<IValueItem>();
	}

	public QueryBuilder() {

	}

	public abstract Querier buildQuerier();

	public void setDistinct() {
		isDistinct = true;
	}

	public void queryAllColumns() {
		columnCaches.add(new AllColumnCache());
	}

	protected void addColumnItem(Expression exp, String alias, String name) {
		checkColumn(name);
		aliases.add(alias);
		if (columnFlag) {
			columns.add(new ColumnSeparatorItem(exp));
		} else {
			columns.add(new ColumnItem(exp));
			columnFlag = true;
		}
	}

	public void addColumn(String name) {
		String alias = name;
		name = toUpper(name);

		columnCaches.add(new ExpressionColumnCache(name, alias, new TextExpression(name)));
	}

	public void addColumn(String tableAlias, String name) {
		String alias = name;
		name = toUpper(name);

		columnCaches.add(new ExpressionColumnCache(name, alias, //
				new TextExpression(Lang.joinStr(tableAlias.length() + name.length() + 1, toUpper(tableAlias), ".", name))));
	}

	public void addAliasColumn(String name, String alias) {
		name = toUpper(name);
		String uAlias = toUpper(alias);

		columnCaches.add(new ExpressionColumnCache(uAlias, alias, //
				new TextExpression(Lang.joinStr(name.length() + 1 + uAlias.length(), name, " ", uAlias))));
	}

	public void addAliasColumn(String tableAlias, String name, String alias) {
		name = toUpper(name);
		tableAlias = toUpper(tableAlias);
		String uAlias = toUpper(alias);

		columnCaches.add(new ExpressionColumnCache(uAlias, alias, //
				new TextExpression(Lang.joinStr(tableAlias.length() + name.length() + uAlias.length() + 2, tableAlias, ".", name, " ", uAlias))));
	}

	public void addAllColumns(String tableAlias) {
		columnCaches.add(new AsteriskColumnCache(tableAlias));
	}

	public void addExpColumn(String alias) {
		String uAlias = toUpper(alias);

		columnCaches.add(new ExpressionColumnCache(uAlias, alias,//
				new AliasExpression(popExp(), uAlias)));
	}

	public void setFrom(String table) {
		table = toUpper(table);

		checkTable(table, table);

		from = new DefaultSource(toUpper(table));
	}

	public void setFrom(String table, String alias) {
		table = toUpper(table);
		alias = toUpper(alias);

		checkTable(table, alias);

		from = new DefaultSource(table, alias);
	}

	public void setSubQueryFrom(Querier querier, String alias) {
		alias = toUpper(alias);

		subQueryAlias = alias;
		String[] subAliases = querier.aliases;
		int size = subAliases.length;
		String[] aliases = new String[size];
		for (int i = 0; i < size;) {
			aliases[i] = toUpper(subAliases[i++]);
		}
		subColumnsAlias = new ArrayIterator<String>(aliases);

		checkTable("", alias);

		from = new SubQuerySource(querier, alias);
	}

	public void addJoin(boolean isInner, String table) {
		table = toUpper(table);

		checkTable(table, table);

		joins().add(new JoinItem(isInner, table, popExp()));
	}

	public void addJoin(boolean isInner, String table, String alias) {
		table = toUpper(table);
		alias = toUpper(alias);

		checkTable(table, alias);

		joins().add(new JoinItem(isInner, table, alias, popExp()));
	}

	public void resolveColumns() {
		for (ColumnCache columnCache : columnCaches) {
			columnCache.resolveColumn(this);
		}
	}

	public void setWhere() {
		where = new Where(popExp());
	}

	protected void addGroupItem(String name) {
		if (groupFlag) {
			groups().add(new GroupItem(Lang.joinStr(name.length() + 1, ",", name)));
		} else {
			groups().add(new GroupItem(name));
			groupFlag = true;
		}
	}

	public void addGroup(String name) {
		addGroupItem(toUpper(name));
	}

	public void addGroup(String tableAlias, String name) {
		addGroupItem(Lang.joinStr(tableAlias.length() + name.length() + 1, toUpper(tableAlias), ".", toUpper(name)));
	}

	public void addAllGroups(String tableAlias) {
		tableAlias = toUpper(tableAlias);
		ArrayIterator<String> iterator = getAllColumns(tableAlias);
		while (iterator.hasNext()) {
			addGroup(tableAlias, iterator.next());
		}
	}

	public void setHaving() {
		having = new Having(popExp());
	}

	public void addUnion(Querier querier) {
		unions().add(new UnionItem(querier, false));
	}

	public void addUnionAll(Querier querier) {
		unions().add(new UnionItem(querier, true));
	}

	public void addAsc() {
		orders().add(new OrderItem(popExp(), true));
	}

	public void addDesc() {
		orders().add(new OrderItem(popExp(), false));
	}

	public void pushSubQuery(Querier querier) {
		push(new SubQueryExpression(querier));
	}

	public void pushID(String id) {
		push(new TextExpression(toUpper(id)));
	}

	public void pushID(String tableAlias, String id) {
		push(new TextExpression(//
				Lang.joinStr(tableAlias.length() + id.length() + 1, toUpper(tableAlias), ".", toUpper(id))));
	}

	public void pushNumber(String number) {
		push(new TextExpression(number));
	}

	public void pushString(String str) {
		push(new TextExpression(str));
	}

	public void pushParam(String name) {
		push(new NamedParamExpression(name));
	}

	public void pushParam(int index) {
		push(new IndexParamExpression(index));
	}

	public void pushAggregation(String name) {
		push(new AggregationExpression(popExp(), toUpper(name), false));
	}

	public void pushDisAggregation(String name) {
		push(new AggregationExpression(popExp(), toUpper(name), true));
	}

	public void pushFunction(String name, int length) {
		Expression[] params = new Expression[length];
		for (int i = length - 1; i > -1;) {
			params[i--] = popExp();
		}
		push(new FunctionExpression(toUpper(name), params));
	}

	public void pushMinus() {
		push(new MinusExpression(popExp()));
	}

	public void pushBracket() {
		push(new BracketExpression(popExp()));
	}

	public void pushMultiplication() {
		push(new TwoExpression(popExp(), popExp(), " * "));
	}

	public void pushDivision() {
		push(new TwoExpression(popExp(), popExp(), " / "));
	}

	public void pushAdd() {
		push(new TwoExpression(popExp(), popExp(), " + "));
	}

	public void pushSubtraction() {
		push(new TwoExpression(popExp(), popExp(), " - "));
	}

	public void pushIfCase(int length) {
		length = length * 2 + 1;
		Expression[] exps = new Expression[length];
		for (int i = length - 1; i > -1;) {
			exps[i--] = popExp();
		}
		push(new IfCaseExpression(exps));
	}

	public void pushSwitchCase(int length) {
		length = length * 2 + 2;
		Expression[] exps = new Expression[length];
		for (int i = length - 1; i > -1;) {
			exps[i--] = popExp();
		}
		push(new SwitchCaseExpression(exps));
	}

	public void pushAny() {
		push(new LeftExpression(popExp(), "ANY"));
	}

	public void pushAll() {
		push(new LeftExpression(popExp(), "ALL"));
	}

	public void pushEQ() {
		push(new TwoExpression(popExp(), popExp(), " = "));
	}

	public void pushLT() {
		push(new TwoExpression(popExp(), popExp(), " < "));
	}

	public void pushGT() {
		push(new TwoExpression(popExp(), popExp(), " > "));
	}

	public void pushLTEQ() {
		push(new TwoExpression(popExp(), popExp(), " <= "));
	}

	public void pushGTEQ() {
		push(new TwoExpression(popExp(), popExp(), " >= "));
	}

	public void pushNotEQ() {
		push(new TwoExpression(popExp(), popExp(), " != "));
	}

	public void pushLike() {
		push(new TwoExpression(popExp(), popExp(), " LIKE "));
	}

	public void pushNotLike() {
		push(new TwoExpression(popExp(), popExp(), " NOT LIKE "));
	}

	public void pushAllLikeParam(String name) {
		push(new NamedAllLikeExpression(name));
	}

	public void pushAllLikeParam(int index) {
		push(new IndexAllLikeExpression(index));
	}

	public void pushLeftLikeParam(String name) {
		push(new NamedLeftLikeExpression(name));
	}

	public void pushLeftLikeParam(int index) {
		push(new IndexLeftLikeExpression(index));
	}

	public void pushIn() {
		push(new TwoExpression(popExp(), popExp(), " IN "));
	}

	public void pushNotIn() {
		push(new TwoExpression(popExp(), popExp(), " NOT IN "));
	}

	public void pushExists() {
		push(new LeftExpression(popExp(), "EXISTS"));
	}

	public void pushNotExists() {
		push(new LeftExpression(popExp(), "NOT EXISTS"));
	}

	public void pushNull() {
		push(new IsNullExpression(popExp()));
	}

	public void pushNotNull() {
		push(new IsNotNullExpression(popExp()));
	}

	public void pushAnd() {
		push(new CompareExpression(popExp(), popExp(), " AND "));
	}

	public void pushOr() {
		push(new CompareExpression(popExp(), popExp(), " OR "));
	}

	public void pushListParam(String name) {
		push(new NamedListExpression(name));
	}

	public void pushListParam(int index) {
		push(new IndexListExpression(index));
	}

	public void pushList(int length) {
		Expression[] params = new Expression[length];
		for (int i = length - 1; i > -1;) {
			params[i--] = popExp();
		}
		push(new ListExpression(params));
	}

	public void pushBetween() {
		push(new BetweenExpression(popExp(), popExp(), popExp()));
	}

	protected ArrayIterator<String> getAllColumns(String name) {
		if (name.equals(subQueryAlias)) {
			subColumnsAlias.reset();
			return subColumnsAlias;
		}
		String table = tableAliasMap.get(name);
		if (table == null) {
			throw Lang.makeThrow("Unknow table '%s'.", name);
		}
		return dataAccesser.getColumns(table);
	}

	protected void checkColumn(String name) {
		if (!columnAliases.add(name)) {
			throw Lang.makeThrow("Repeated column alias: '%s'.", name);
		}
	}

	protected void checkTable(String table, String alias) {
		if (tableAliasMap.put(alias, table) != null) {
			throw Lang.makeThrow("Repeated source: '%s'", alias);
		}
	}

	protected Expression popExp() {
		return expStack[--expStackPo];
	}

	protected void push(Expression node) {
		growStack(expStackPo + 2);
		expStack[expStackPo++] = node;
	}

	protected void growStack(int minCapacity) {
		if (minCapacity - expStack.length > 0) {
			int oldCapacity = expStack.length;
			int newCapacity = oldCapacity + (oldCapacity >> 1);
			if (newCapacity - minCapacity < 0) {
				newCapacity = minCapacity;
			}
			expStack = Arrays.copyOf(expStack, newCapacity);
		}
	}

	protected List<JoinItem> joins() {
		return joins == null ? joins = new ArrayList<JoinItem>() : joins;
	}

	protected List<GroupItem> groups() {
		return groups == null ? groups = new ArrayList<GroupItem>(5) : groups;
	}

	protected List<OrderItem> orders() {
		return orders == null ? orders = new ArrayList<OrderItem>(5) : orders;
	}

	protected List<UnionItem> unions() {
		return unions == null ? unions = new ArrayList<UnionItem>(5) : unions;
	}

	public void setValue(String name) {
		if (columnFlag) {
			uValues.add(new UValueItem(toUpper(name), popExp(), false));
		} else {
			uValues.add(new UValueItem(toUpper(name), popExp(), true));
			columnFlag = true;
		}
	}

	public void insertValue(String name) {
		if (subQueryAlias.equals(name)) {
			return;
		}
		iValues.add(new IValueItem(toUpper(name), popExp()));
	}

	public UValue buildUValue(int paramIndex) {
		return new UValue(uValues.toArray(new UValueItem[uValues.size()]), paramIndex);
	}

	public IValue buildIValue() {
		final int size = iValues.size();
		final IValueItem[] items = iValues.toArray(new IValueItem[size]);
		StringBuilder sqlBuilder = new StringBuilder("INSERT INTO ").append(table).append('(').append(items[0].getName());
		for (int i = 1; i < size; i++) {
			sqlBuilder.append(',').append(items[i].getName());
		}
		sqlBuilder.append(") VALUES (");
		final String preffix = sqlBuilder.toString();
		return new IValue() {

			@Override
			public void visit(SQLVisitor visitor) {
				visitor.appendSQL(preffix);
				items[0].visitExp(visitor);
				for (int i = 1; i < size; i++) {
					visitor.appendSQL(',');
					items[i].visitExp(visitor);
				}
				visitor.appendSQL(')');
			}
		};
	}

	public static String toUpper(String str) {
		char[] chars = str.toCharArray();
		int length = chars.length;
		char c;
		for (int i = 0; i < length; i++) {
			c = chars[i];
			if (c > 96 && c < 123) {
				chars[i] = (char) (c ^ 32);
			}
		}
		return new String(chars);
	}

}
